查看原文
其他

揭秘 Kotlin 1.6.20 重磅功能 Context Receivers

DHL ByteCode 2022-12-14



hi 这是 dhl 的第 65 篇原创文章

个人微信: hi-dhl

hi 大家好,我是 DHL。公众号:ByteCode ,专注分享有趣硬核原创内容,Kotlin、Jetpack、性能优化、系统源码、算法及数据结构、动画、大厂面经。

这篇文章我们一起来聊一下  Kotlin 1.6.20 的新功能 Context Receivers,来看看它为我们解决了什么问题。

通过这篇文章将会学习到以下内容:

  • 扩展函数的局限性

  • 什么是 Context Receivers,以及如何使用

  • Context Receivers 解决了什么问题

  • 引入 Context Receivers 会带来新的问题,我们如何解决

  • Context Receivers 应用范围及注意事项

扩展函数的局限性

在 Kotlin 中接受者只能应用在扩展函数或者带接受者 lambda 表达式中, 如下所示。

class Context {
  var density = 0f
}

// 扩展函数
inline fun Context.px2dp(value: Int): Float = value.toFloat() / density

接受者是 fun 关键字之后点之前的类型 Context,这里隐藏了两个知识点。

  • 我们可以像调用内部函数一样,调用扩展函数 px2dp(),通常结合 Kotlin 作用域函数 with , run ,  apply 等等一起使用。

with(Context()) {
  px2dp(100)
}
  • 在扩展函数内部,我们可以使用 this 关键字,或者隐藏关键字隐式访问内部的成员函数,但是我们不能访问私有成员

扩展函数使用起来很方便,我们可以对系统或者第三方库进行扩展,但是也有局限性。

  • 只能定义一个接受者,因此限制了它的可组合性,如果有多个接受者只能当做参数传递。比如我们调用 px2dp() 方法的同时,往 logcatfile 中写入日志。

class LogContext {
  fun logcat(message: Any){}
}
class FileContext {
  fun writeFile(message: Any) {}
}

fun printf(logContext: LogContext, fileContext: FileContext) {
  with(Context()) {
      val dp = px2dp(100)
      logContext.logcat("print ${dp} in logcat")
      fileContext.writeFile("write ${dp} in file")
  }
}
  • 在 Kotlin 中接受者只能应用在扩展函数或者带接受者 lambda 表达式中,却不能在普通函数中使用,失去了灵活性

Context Receivers 的出现带来新的可能性,它通过了组合的方式,将多个上下文接受者合并在一起,灵活性更高,应用范围更广。

什么是 Context Receivers

Context Receivers 用于表示一个基本约束,即在某些情况下需要在某些范围内才能完成的事情,它更加的灵活,可以通过组合的方式,组织上下文,将系统或者第三方类组合在一起,实现更多的功能。

如果想在项目中使用 Context Receivers,需要将 Kotlin 插件升级到 1.6.20 ,并且在项目中开启才可以使用。

plugins {
  id 'org.jetbrains.kotlin.jvm' version '1.6.20'
}
// ......
kotlinOptions {
  freeCompilerArgs = ["-Xcontext-receivers"]
}

如何使用 Context Receivers

当我们完成上述配置之后,就可以在项目中使用 Context Receivers,现在我们将上面的案例改造一下。

context(LogContext, FileContext)
fun printf() {
  with(Context()) {
      val dp = px2dp(100)
      logContext.logcat("print ${dp} in logcat")
      fileContext.writeFile("write ${dp} in file")
  }
}

我们在 printf() 函数上,使用  context() 关键字,在 context() 关键字括号中,声明上下文接收者类型的列表,多个类型用逗号分隔。但是列出的类型不允许重复,它们之间不允许有子类型关系。

通过 context() 关键字来限制它的作用范围,在这个函数中,我们可以调用上下文 LogContextFileContext 内部的方法,但是使用的时候,只能通过 Kotlin 作用域函数嵌套来传递多个接受者,也许在未来可能会提供更加优雅的方式。

with(LogContext()) {
  with(FileContext()) {
      printf("I am DHL")
  }
}

引入 Context Receivers 导致可读性问题

如果我们在 LogContextFileContext 中声明了多个相同名字的变量或者函数,我们只能通过 this@Lable 语句来解决这个问题。

context(LogContext, FileContext)
fun printf(message: String) {
  logcat("print message in logcat ${this@LogContext.name}")
  writeFile("write message in file ${this@FileContext.name}")
}

正如你所见,在 LogContextFileContext 中都有一个名为 name 的变量,我们只能通过 this@Lable 语句来访问,但是这样会引入一个新的问题,如果有大量的同名的变量或者函数,会导致 this 关键字分散到处都是,造成可读性很差。所以我们可以通过接口隔离的方式,来解决这个问题。

interface LogContextInterface{
  val logContext:LogContext
}
interface FileContextInterface{
  val fileContext:FileContext
}

context(LogContextInterface, FileContextInterface)
fun printf(message: String) {
  logContext.logcat("print message in logcat ${logContext.name}")
  fileContext.writeFile("write message in file ${fileContext.name}")
}

通过接口隔离的方式,我们就可以解决 this 关键字导致的可读性差的问题,使用的时候需要实例化接口。

val logContext = object : LogContextInterface {
  override val logContext: LogContext = LogContext()
}
val fileContext = object : FileContextInterface {
  override val fileContext: FileContext = FileContext()
}

with(logContext) {
  with(fileContext) {
      printf("I am DHL")
  }
}

Context Receivers 应用范围及注意事项

当我们重写带有上下文接受者的函数时,必须声明为相同类型的上下文接受者。

interface Canvas
interface Shape {
  context(Canvas)
  fun draw()
}

class Circle : Shape {
  context(Canvas)
  override fun draw() {
  }
}

我们重写了 draw() 函数,声明的上下文接受者必须是相同的,Context Receivers 不仅可以作用在扩展函数、普通函数上,而且还可以作用在类上。

context(LogContextInterface, FileContextInterface)
class LogHelp{
  fun printf(message: String) {
      logContext.logcat("print message in logcat ${logContext.name}")
      fileContext.writeFile("write message in file ${fileContext.name}")
  }
}

在类 LogHelp 上使用了 context() 关键字,我们就可以在 LogHelp 范围内任意的地方使用 LogContext 或者 FileContex

val logHelp = with(logContext) {
  with(fileContext) {
      LogHelp()
  }
}
logHelp.printf("I am DHL")

Context Receivers 除了作用在扩展函数、普通函数、类上,还可以作用在属性 gettersetter 以及 lambda 表达式上。

context(View)
val Int.dp get() = this.toFloat().dp

// lambda 表达式
fun save(block: context(LogContextInterface) () -> Unit) {
}

最后我们来看一下,来自社区 Context Receivers 实践的案例,扩展 Json 工具类。

fun json(build: JSONObject.() -> Unit) = JSONObject().apply { build() }

context(JSONObject)
infix fun String.by(build: JSONObject.() -> Unit) = put(this, JSONObject().build())

context(JSONObject)
infix fun String.by(value: Any) = put(this, value)

fun main() {
  val json = json {
      "name" by "Kotlin"
      "age" by 10
      "creator" by {
          "name" by "JetBrains"
          "age" by "21"
      }
  }
}

总结

  • Context Receivers 提供一个基本的约束,可以在指定范围内,通过组合的方式实现更多的功能

  • Context Receivers 可以作用在扩展函数、普通函数、类、属性 gettersetterlambda 表达式

  • Context Receivers 允许在不需要继承的情况,通过组合的方式,组织上下文,将系统或者第三方类组合在一起,实现更多的功能

  • 通过 context() 关键字声明,在 context() 关键字括号中,声明上下文接收者类型的列表,多个类型用逗号分隔

  • 如果大量使用 this 关键字会导致可读性变差,我们可以通过接口隔离的方式来解决这个问题

  • 当我们重写带有上下文接受者的函数时,必须声明为相同类型的上下文接受者


推荐阅读

Stack Overflow 上最热门的 10 个 Kotlin 问题?

Android 12 已来,你的 App 崩溃了吗?
Android 11 提高 App 冷启动速度 5% 以上


全文到这里就结束了,如果对你有帮助,欢迎 在看点赞收藏分享 给身边的朋友。


公众号:ByteCode ,分享有用、有趣的硬核原创内容,Kotlin、Jetpack、性能优化、系统源码、算法及数据结构、动画、大厂面经。

往期视频


视频号:一个认真分享 技术 、 工具 、 效率 相关内容的频道
  点击查看代码字符视频实现教程  

👇🏻 真诚推荐你关注我👇🏻

因微信公众号更改了推送机制

可能无法及时看到最新文章

 将公众号设为 星标 

或常为文章点 在看

即可及时收到最新文章


欢迎前往 博客 查看更多 Kotlin、Jetpack 、动画算法图解、系统源码分析等等文章。以及开源项目、LeetCode / 剑指 offer / 国内外大厂面试题 / 多线程 题解。

https://www.hi-dhl.com

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存